Android 通过自己创建Resources加载非身生apk的资源

在我们开发中我们在xml里面写布局

1
2
3
4
<ImageView
android:src="@mipmap/ic_launcher_round"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

我们的src中赋值@mipmap/ic_launcher_round,然后就会得到对应的资源,那么有没有好奇我们这些资源是怎么加载进去的呢?我们点进ImageView去查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
       final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}

@Nullable
public Drawable getDrawable(@StyleableRes int index) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}

final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
return null;
}

mResources.loadDrawable(value, value.resourceId, mTheme);

调用的是mResourcesloadDrawable方法,这样就获取了相应的值。下面我们通过来分析Resources是怎么创建和寻找对应的资源的。如果我们想要用自己的Resources来加载资源,我们应该怎么做呢?
我们从ActivitygetResources()里面入手:

1
2
3
4
5
6
7
8
9
10
11
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}

可以发现这个mResources是在Context里面获取到的,在Context里面:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Returns a Resources instance for the application's package.
* <p>
* <strong>Note:</strong> Implementations of this method should return
* a Resources instance that is consistent with the AssetManager instance
* returned by {@link #getAssets()}. For example, they should share the
* same {@link Configuration} object.
*
* @return a Resources instance for the application's package
* @see #getAssets()
*/
public abstract Resources getResources();

这是一个抽象方法,从注释上我们可以知道这个应该和getAssets(),也就是和AssetManager有关,我们找到Context的实现类ContextImpl查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  @Override
public Resources getResources() {
return mResources;
}
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
//....
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
}
}
mResources = resources;
//....
}

发现是ResourcesManager里面调用了getTopLevelResources函数,点进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* Creates the top level Resources for applications with the given compatibility info.
*
* @param resDir the resource directory.
* @param splitResDirs split resource directories.
* @param overlayDirs the resource overlay directories.
* @param libDirs the shared library resource dirs this app references.
* @param displayId display Id.
* @param overrideConfiguration override configurations.
* @param compatInfo the compatibility info. Must not be null.
*/
Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
//.....


Configuration overrideConfigCopy = (overrideConfiguration != null)
? new Configuration(overrideConfiguration) : null;
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
Resources r;
synchronized (this) {
// Resources is app scale dependent.
if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);

WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
//if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
if (r != null && r.getAssets().isUpToDate()) {
if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
+ ": appScale=" + r.getCompatibilityInfo().applicationScale
+ " key=" + key + " overrideConfig=" + overrideConfiguration);
return r;
}
}
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}

if (splitResDirs != null) {
for (String splitResDir : splitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
return null;
}
}
}

if (overlayDirs != null) {
for (String idmapPath : overlayDirs) {
assets.addOverlayPath(idmapPath);
}
}

if (libDirs != null) {
for (String libDir : libDirs) {
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
if (assets.addAssetPath(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}

//Log.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
DisplayMetrics dm = getDisplayMetricsLocked(displayId);
Configuration config;
final boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
final boolean hasOverrideConfig = key.hasOverrideConfiguration();
if (!isDefaultDisplay || hasOverrideConfig) {
config = new Configuration(getConfiguration());
if (!isDefaultDisplay) {
applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
}
if (hasOverrideConfig) {
config.updateFrom(key.mOverrideConfiguration);
if (DEBUG) Slog.v(TAG, "Applied overrideConfig=" + key.mOverrideConfiguration);
}
} else {
config = getConfiguration();
}
r = new Resources(assets, dm, config, compatInfo);
//....
return r;
}
}

从这段代码我们就可以知道,如果缓存里面有对应的Resources就直接返回,如果没有就直接new出来,对应的资源路径就是通过AssetManager.addAssetPath(String path)函数设置,也验证了上面说的那句话,ResourcesAssetManager有关。

AssetManager的创建

我们进入AssetManager类中查看addAssetPath函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}

private native final int addAssetPathNative(String path);

可以看到调用了一个native方法,从注释我们可以知道,这个路径对应的可以是一个目录或者一个zip文件。我们看看AssetManager的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Create a new AssetManager containing only the basic system assets.
* Applications will not generally use this method, instead retrieving the
* appropriate asset manager with {@link Resources#getAssets}. Not for
* use by applications.
* {@hide}
*/
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init(false);
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}

我们可以查看到addAssetPath这个方法和AssetManager的构造方法注释上面都有{@hide},这个表示我们不能在应用层直接调用,如果我们还是要调用的话就需要用到反射:

1
2
3
4
5
6
7
//创建一个AssetManager
//AssetManager assetManager = new AssetManager(); hide的调用不了 只有用反射调用
AssetManager assetManager = AssetManager.class.newInstance();
//添加本地下载好的资源
// assetManager.addAssetPath(String path);// 也是hide的调用不了 继续用反射执行该方法
Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, resPath);

  通过前面的分析可知,Android系统中实际对资源的管理是AssetManager类.每个Resources对象都会关联一个AssetManager对象,Resources将对资源的操作大多数委托给了AssetManager。当然有些源码还有一层 ResourcesImpl 刚刚我们也看到了。
  另外还会存在一个native层的AssetManager对象与java层的这个AssetManager对象相对应,而这个native层AssetManager对象在内存的地址存储在java层的AssetManager.mObject中。所以在java层AssetManager的jni方法中可以快速找到它对应的native层的AssetManager对象。
AssetManager的init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Create a new AssetManager containing only the basic system assets.
* Applications will not generally use this method, instead retrieving the
* appropriate asset manager with {@link Resources#getAssets}. Not for
* use by applications.
* {@hide}
*/
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init(false);
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}

// ndk的源码路径
// frameworks/base/core/jni/android_util_AssetManager.cpp
// frameworks/base/libs/androidfw/AssetManager.cpp
private native final void init(boolean isSystem);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
if (isSystem) {
verifySystemIdmaps();
}
// AssetManager.cpp
AssetManager* am = new AssetManager();
if (am == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", "");
return;
}

am->addDefaultAssets();

ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
}

bool AssetManager::addDefaultAssets()
{
const char* root = getenv("ANDROID_ROOT");
LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

String8 path(root);
// framework/framework-res.apk
// 初始化的时候会去加载系统的framework-res.apk资源
// 也就是说我们为什么能加载系统的资源如颜色、图片、文字等等
path.appendPath(kSystemAssets);

return addAssetPath(path, NULL);
}

AssetManageraddAssetPath(String path)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
asset_path ap;

// 省略一些校验代码

// 判断是否已经加载过了
for (size_t i=0; i<mAssetPaths.size(); i++) {
if (mAssetPaths[i].path == ap.path) {
if (cookie) {
*cookie = static_cast<int32_t>(i+1);
}
return true;
}
}

// 检查路径是否有一个androidmanifest . xml
Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
kAndroidManifest, Asset::ACCESS_BUFFER, ap);
if (manifestAsset == NULL) {
// 如果不包含任何资源
delete manifestAsset;
return false;
}
delete manifestAsset;
// 添加
mAssetPaths.add(ap);

// 新路径总是补充到最后
if (cookie) {
*cookie = static_cast<int32_t>(mAssetPaths.size());
}

if (mResources != NULL) {
appendPathToResTable(ap);
}

return true;
}

bool AssetManager::appendPathToResTable(const asset_path& ap) const {
// skip those ap's that correspond to system overlays
if (ap.isSystemOverlay) {
return true;
}

Asset* ass = NULL;
ResTable* sharedRes = NULL;
bool shared = true;
bool onlyEmptyResources = true;
MY_TRACE_BEGIN(ap.path.string());
// 资源覆盖机制,暂不考虑
Asset* idmap = openIdmapLocked(ap);
size_t nextEntryIdx = mResources->getTableCount();
ALOGV("Looking for resource asset in '%s'\n", ap.path.string());
// 资源包路径不是一个文件夹,那就是一个apk文件了
if (ap.type != kFileTypeDirectory) {
// 对于app来说,第一次执行时,肯定为0,因为mResources刚创建,还没对其操作
// 下面的分支 指挥在参数是系统资源包路径时,才执行,
// 而且系统资源包路径是首次被解析的
// 第二次执行appendPathToResTable,nextEntryIdx就不会为0了
if (nextEntryIdx == 0) {
// mAssetPaths中存储的第一个资源包路径是系统资源的路径,
// 即framework-res.apk的路径,它在zygote启动时已经加载了
// 可以通过mZipSet.getZipResourceTable获得其ResTable对象
sharedRes = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTable(ap.path);
// 对于APP来说,肯定不为NULL
if (sharedRes != NULL) {
// 得到系统资源包路径中resources.arsc个数
nextEntryIdx = sharedRes->getTableCount();
}
}
// 当参数是mAssetPaths中除第一个以外的其他资源资源包路径,
// 比如app自己的资源包路径时,走下面的逻辑
if (sharedRes == NULL) {
// 检查该资源包是否被其他进程加载了,这与ZipSet数据结构有关,后面在详细介绍
ass = const_cast<AssetManager*>(this)->
mZipSet.getZipResourceTableAsset(ap.path);
// 对于app自己的资源包来说,一般都会都下面的逻辑
if (ass == NULL) {
ALOGV("loading resource table %s\n", ap.path.string());
// 创建Asset对象,就是打开resources.arsc
ass = const_cast<AssetManager*>(this)->
openNonAssetInPathLocked("resources.arsc",
Asset::ACCESS_BUFFER,
ap);
if (ass != NULL && ass != kExcludedAsset) {
ass = const_cast<AssetManager*>(this)->
mZipSet.setZipResourceTableAsset(ap.path, ass);
}
}
// 只有在zygote启动时,才会执行下面的逻辑
// 为系统资源创建 ResTable,并加入到mZipSet里。
if (nextEntryIdx == 0 && ass != NULL) {
// If this is the first resource table in the asset
// manager, then we are going to cache it so that we
// can quickly copy it out for others.
ALOGV("Creating shared resources for %s", ap.path.string());
// 创建ResTable对象,并把前面与resources.arsc关联的Asset对象,加入到这个ResTabl中
sharedRes = new ResTable();
sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
sharedRes = const_cast<AssetManager*>(this)->
mZipSet.setZipResourceTable(ap.path, sharedRes);
}
}
} else {
ALOGV("loading resource table %s\n", ap.path.string());
ass = const_cast<AssetManager*>(this)->
openNonAssetInPathLocked("resources.arsc",
Asset::ACCESS_BUFFER,
ap);
shared = false;
}

if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
ALOGV("Installing resource asset %p in to table %p\n", ass, mResources);
// 系统资源包时
if (sharedRes != NULL) {
ALOGV("Copying existing resources for %s", ap.path.string());
mResources->add(sharedRes);
} else {
// 非系统资源包时,将与resources.arsc关联的Asset对象加入到Restable中
// 此过程会解析resources.arsc文件。
ALOGV("Parsing resources for %s", ap.path.string());
mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
}
onlyEmptyResources = false;

if (!shared) {
delete ass;
}
} else {
mResources->addEmpty(nextEntryIdx + 1);
}

if (idmap != NULL) {
delete idmap;
}
MY_TRACE_END();

return onlyEmptyResources;
}

大家应该之前了解过这个文件resources.arsc, 如果没了解过可以在网上找篇文章看一下。apk在打包的时候会生成它,我们解压apk就应该能够看到他。这里面基本都是存放的资源的索引,之所以不同的分辨率可以加载不同的图片它可是个大功臣。

我们获取到了assetManager其他的都好办了,因为资源地址是由这个决定的,所以我们其他的都可以用当前app本身的

1
2
3
4
5
6
7
8
9
//创建一个AssetManager
//AssetManager assetManager = new AssetManager(); hide的调用不了 只有用反射调用
AssetManager assetManager = AssetManager.class.newInstance();
//添加本地下载好的资源皮肤
// assetManager.addAssetPath(String path);// 也是hide的调用不了 继续用反射执行该方法
Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, resPath);
Resources superResources = context.getResources();
Resources mResources = new Resources(assetManager, superResources.getDisplayMetrics(), superResources.getConfiguration());

这样的话我们就可以通过自己创建Resources来加载非本身apk的资源了
`

-------------The End-------------